using System.Runtime.CompilerServices;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using NSwag.Annotations;
using ProxyServer.Certs;
using ProxyServer.EntityFrameworkCore;
using ProxyServer.Kestrel;
using ProxyServer.Models;

namespace ProxyServer.Endpoints;

public static class CertsEndpointsExtensions
{
    public static IEndpointRouteBuilder MapCertsEndpoints(this IEndpointRouteBuilder endpoints)
    {
        var certs = endpoints.MapGroup("/certs").WithTags("Certificates");

        certs.MapPost("/upload", UploadCertificate).DisableAntiforgery();
        certs.MapPost("/", ListCertificates);
        certs.MapDelete("/{id:guid}", DeleteCertificate);

        return certs;
    }

    [OpenApiOperation(nameof(DeleteCertificate), "", "")]
    public static async Task<Results<Ok, NotFound, BadRequest>> DeleteCertificate([FromRoute] Guid id, [FromServices] AppDbContext appDb, [FromServices] ILoggerFactory loggerFactory, CancellationToken cancellationToken)
    {
        var logger = loggerFactory.CreateLogger("ProxyServer.Certs.Endpoints.DeleteCertificate");

        var certInfo = await appDb.CertificateInfos.FindAsync([id], cancellationToken);
        if (string.IsNullOrWhiteSpace(certInfo?.Path))
        {
            logger.LogInformation("Not found storage path for {Id} in database", id);
            return TypedResults.NotFound();
        }

        if (File.Exists(certInfo.Path) == false)
        {
            logger.LogInformation("File not found: {FilePath}", certInfo.Path);
            return TypedResults.NotFound();
        }

        certInfo.Deleted = true;
        File.Delete(certInfo.Path);
        await appDb.SaveChangesAsync(cancellationToken);

        return TypedResults.Ok();
    }

    static string LoggerName([CallerMemberName] string? caller = default) => $"ProxyServer.Certs.Endpoints.{caller}";

    [OpenApiOperation(nameof(UploadCertificate), "", "")]
    public static async Task<Results<Ok, InternalServerError>> UploadCertificate(
        IFormFile uploadFile,
        [FromServices] IWebHostEnvironment env,
        [FromQuery] string? password,
        [FromServices] AppDbContext appDb,
        [FromServices] ILoggerFactory loggerFactory,
        [FromServices] CertificateSelector selector,
        CancellationToken cancellationToken)
    {
        var logger = loggerFactory.CreateLogger(LoggerName());

        var fileExt = Path.GetExtension(uploadFile.FileName);
        var dir = Path.Combine(env.ContentRootPath, "App_Data", "certs");
        if (Directory.Exists(dir) == false)
        {
            Directory.CreateDirectory(dir);
        }

        var fileId = Guid.NewGuid();
        var filePath = Path.GetFullPath(Path.Combine(dir, $"{fileId:N}{fileExt}"));

        using var ms = new MemoryStream();
        using (var uploadStream = uploadFile.OpenReadStream())
        {
            await uploadStream.CopyToAsync(ms);
        }
        ReadOnlySpan<byte> fileBytes = ms.ToArray();

        var cert = fileExt switch
        {
            ".crt" => X509CertificateLoader.LoadCertificate(fileBytes),
            ".pfx" => X509CertificateLoader.LoadPkcs12(
                fileBytes,
                string.IsNullOrWhiteSpace(password) ? null : password,
                X509KeyStorageFlags.Exportable),
            _ => throw new NotSupportedException(fileExt)
        };

        logger.LogInformation("SerialNumber: {Value}, HasPrivateKey: {HasPrivateKey}", cert.GetSerialNumberString(), cert.HasPrivateKey);

        var certInfo = await appDb.CertificateInfos
            .Where(x => x.Thumbprint == cert.Thumbprint && x.Deleted == false).FirstOrDefaultAsync(cancellationToken);

        if (certInfo is not null)
        {
            return TypedResults.Ok();
        }

        certInfo = new CertificateInfo(fileId)
        {
            SubjectName = cert.Subject,
            IssuerName = cert.Issuer,
            Password = password,
            Expires = cert.NotAfter,
            FriendlyName = cert.FriendlyName,
            Path = filePath,
            Thumbprint = cert.Thumbprint
        };

        appDb.CertificateInfos.Add(certInfo);
        await appDb.SaveChangesAsync(cancellationToken);

        using var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write);
        ms.Position = 0; ms.Seek(0, SeekOrigin.Begin);
        await ms.CopyToAsync(fs);
        selector.Add(cert);

        return TypedResults.Ok();
    }

    [OpenApiOperation(nameof(ListCertificates), "", "")]
    public static async Task<PagedResult<CertificateInfo>> ListCertificates([FromBody] PageRequest request, [FromServices] AppDbContext appDb, CancellationToken cancellationToken)
    {
        var query = appDb.CertificateInfos.AsNoTracking();
        var totalCount = await query.CountAsync(cancellationToken);
        var datas = await appDb.CertificateInfos
            .OrderByDescending(x => x.CreatedTime)
            .Skip(request.Offset)
            .Take(request.PageSize)
            .ToListAsync(cancellationToken);

        return new PagedResult<CertificateInfo>(totalCount, datas);
    }
}